Skip to content

draft: UTXO reservations — segregated custody with in-kind redemption - #1088

Draft
mswilkison wants to merge 19 commits into
mainfrom
feat/utxo-reservation-core
Draft

draft: UTXO reservations — segregated custody with in-kind redemption#1088
mswilkison wants to merge 19 commits into
mainfrom
feat/utxo-reservation-core

Conversation

@mswilkison

@mswilkison mswilkison commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What this is

Draft implementation of UTXO reservations: a deposit lane where a depositor's coins are custodied by the threshold network without ever commingling with the pooled supply, and redemption returns exactly their coin lineage (unbroken 1-input-1-output spends from deposit to redemption). Motivations: bailment-style fact pattern for tax treatment of bridging, and provable client-level fund segregation for institutions — with the side benefit that reserved clients neither inherit nor contribute UTXO taint.

Wallet-side companion: threshold-network/keep-core#4238.

Design

Commingling in tBTC happens at exactly one place: the deposit sweep merges deposit UTXOs into the wallet's main UTXO, and balances are credited only there. A reservation is a deposit that is never merged:

  1. Reveal: a regular reveal routed to the new ReservationVault (reveal.vault doubles as the reservation flag).
  2. Anchor: instead of sweeping, the wallet performs a 1-in-1-out spend of the deposit into a fresh wallet-controlled output with no refund path. Credit happens only on the anchor's SPV proof — the sweep's refund-disabling role without its consolidating role.
  3. Custody term: a contract-layer fact only (no Bitcoin-side timelocks). Extension is a fee payment.
  4. In-kind redemption: gross surrender, wallet spends exactly the anchor outpoint to the redeemer script, gross burn reconciles supply to backing to the satoshi.
  5. Re-anchor (wallet migration) and dissolution (post term+grace; the stranding pressure valve).

Claims are minted gross, never netted — all protocol fees are explicit TBTC transfers collected by the vault (schedule: 40 bps initiation + 20 bps/yr extension + 20 bps redemption, decomposing as endpoint parity with the pooled path — a 20 bps mint leg and the standard 20 bps redemption fee — plus a 20 bps/yr custody fee that is the actual premium being purchased, with the first year prepaid inside the initiation fee. An N-year holding pays 40 + 20N bps against the pooled 40 bps round trip: strictly premium at every horizon. The redemption fee is waived on retries after wallet-fault timeouts. The schedule is intended to be stable across the FROST transition: the minimum reservation size, not the fee legs, is the governance dial that keeps carry covering per-position lifecycle costs).

Safety wiring

  • Acceptance marks the deposit sweptAt; consumed anchors are recorded in spentMainUTXOs — fraud-defeat coverage with zero Fraud.sol changes.
  • Sweep proofs targeting the reservation vault revert (Bridge) and sweep proposals containing reserved deposits are rejected (WalletProposalValidator).
  • Wallets cannot finalize closing while custodying reservations.
  • Watchtower integration is complete on both sides: requests pass isSafeRedemption, and guardians can veto pending reserved redemptions via RedemptionWatchtower.raiseReservedObjection (same three-objection flow, freeze, penalty burn, and redeemer ban; reuses the existing veto storage keyed by reservation key — no layout changes, upgrade-safe). Reserved vetoes deliberately share the pooled veto parameters and semantics (penalty divisor, freeze period, ban); parity is intended, not inherited by accident. Timeouts reuse notifyWalletRedemptionTimeout slashing.
  • WalletProposalValidator validates all four lifecycle proposals (anchor eligibility/fees/refund margin, reserved redemption age/timeout margins/watchtower delay, re-anchor targets, dissolution term+grace).
  • BridgeGovernance stages all reservation parameters behind the standard governance delay (beginReservationParametersUpdate / finalizeReservationParametersUpdate).
  • Premature dissolution signing is economically deterred (undefeatable fraud-challenge target until grace passes).
  • Launch throttles: min size, total cap, per-wallet cap — defaulting to disabled until governance wires them.

Testing

26 tests in the reservation suite; full repo suite green (3000+). Includes Bitcoin SPV fixtures for all four proof validators (structurally-valid transactions + regtest-difficulty headers crafted in TypeScript; SPV checks structure/merkle/PoW, not scripts), the watchtower veto e2e (three guardian objections through the Bridge hook, penalty burn, ban blocking re-requests), validator coverage, vault fee math, retry path, and the governance-delayed parameter flow.

Decision point 1: EIP-170 strategy

Main's Bridge compiles to 24.093 KB at runs=1000 — 483 bytes under the 24.576 limit. No usable reservation API fits in that. This PR consolidates all four lifecycle proofs behind one submitReservationProof entry point, trims the API to essentials, and gives Bridge.sol a runs=100 optimizer override (a contract-specific override — the same technique BridgeGovernance uses, though it runs at 200, not the same number), landing at 24.175 KB with the full surface including the veto hook and parameters getter.

  • Cost of runs=100: marginally higher runtime gas on all Bridge functions (typically low single-digit percent on hot paths — worth quantifying with a gas-report diff before mainnet).
  • Alternative: a router-style refactor moving entry points out of the Bridge — durable headroom, but it would stand up a second router architecture parallel to the one the P2TR activation track is already building.

Recommendation: ship the override short-term; converge on the activation track's router architecture as the durable fix rather than inventing a parallel one. The override is one line, reversible, and precedented; the router is the right end-state but should happen once, in one shape.

Decision point 2: re-anchor miner-fee policy

Re-anchor (wallet migration) miner fees ride in-kind: they reduce the on-chain anchorAmount while the gross claim (mintedAmount) is unchanged, so accumulated fees are settled by the owner at redemption (gross burn vs. reduced payout). Migrations happen on the network's schedule, so this charges clients for events they don't control — but the protocol invariant stays maximally clean ("claims burn gross; miner fees are the only in-kind deduction"), and the amounts are negligible: a re-anchor costs roughly 500–2,000 sats, i.e. sub-0.01 bp on a 10 BTC reservation, versus 20 bps/yr custody.

  • Alternative: vault-side compensation (transfer the fee delta to the owner at redemption from custody-fee revenue) — requires the vault to retain a fee buffer and adds a transfer path + audit surface for immaterial fairness gain.

Recommendation: keep owner-borne in the protocol; disclose in the custody agreement that all Bitcoin miner fees — including network-scheduled re-anchors — ride in-kind, and handle any client-specific rebates commercially. The design principle that rotation must not be disincentivized is preserved where it matters: operator economics are untouched.

⚠️ Comprehensive review outcome — NOT audit-ready as-is

A full design+implementation review (Codex gpt-5.6-sol, max effort) found 1 Critical, 8 High, 9 Medium, 1 Low. The SPV proof paths, replay guards, and storage layout are sound; the findings are about the settlement state machine and economic model. Root cause: the reservation lifecycle is single-phase (request → prove) over long-lived per-position UTXOs, but tBTC settlement needs a two-phase authorize-then-prove model with request nonces and terminal settlement records (as the main redemption path has via timedOutRedemptions). The headline bug (C-01) is a claim double-spend: a timeout/veto racing a confirmed reserved-redemption tx refunds the claim after the BTC was already paid.

Critical: C-01 claim double-spend (timeout/veto races a confirmed redemption tx -> BTC paid + claim refunded).
High: H-01 re-anchor/dissolution lack on-chain authorization + proof-type ambiguity; H-02 capacity/lifecycle checked at proof time not reserved before signing (confirmed spends become unprovable); H-03 watchtower delay enforced only by the advisory validator, not the Bridge proof path; H-04 dissolution permanently underbacks by cumulative in-kind fees (treasury transfers do not burn liability); H-05 requests against Closing wallets permanently lock owner+wallet and post-grace requests defeat the stranding bound; H-06 forced termination with live anchors enables unchallengeable unbacked mint; H-07 concurrent no-main-UTXO dissolutions race; H-08 FIXED (re-anchor-floor regression).
Medium (9): retry fee-bypass, per-generation veto keying, vault-swap sweep leak, anchor-not-bound-to-named-wallet, cap semantics, term/fee bounds, instant vault fee changes + un-transferred ownership, release completeness. Low (1): stale close data.

The Critical/High settlement-race items are a deliberate follow-up redesign (two-phase authorize-then-prove with request nonces + terminal settlement records), not ad-hoc patches. Full triaged detail with file:line and fixes is maintained in the local design docs.

External review notes (Codex gpt-5.6-sol, max effort)

An independent read of the proof validators surfaced two correctness bugs, now fixed in this branch with regression tests:

  • Redemption underflow: the redemption range check subtracted the governable redemptionTxMaxFee from anchorAmount, reverting on underflow when a governance fee increase pushes the bound above an existing anchor — which would strand that reservation. Reformulated underflow-safe.
  • Missing re-anchor floor: re-anchors bounded only the per-hop fee; repeated network-scheduled hops could grind the anchor toward the fee bound. Now floored at reservationMinAmount.

Open items it raised, carried as review/governance decisions (not blockers):

  • Dissolution deficit is exact-plus-fee: on dissolution the pooled shortfall is mintedAmount − anchorAmount + dissolutionTxFee, and reservationTotalAmount tracks current anchor assets rather than gross minted liability, so the total-reserved cap does not bound cumulative backing leakage across many dissolved positions. Immaterial at the sat-scale magnitudes and the launch cap, but worth an explicit acknowledgement (or a small burn-the-shortfall step) before the cap is widened.
  • Fees are not grandfathered: ReservationVault.updateFees applies immediately to live positions (each leg ≤ 500 bp). If the custody agreement promises schedule stability, this needs a timelock/snapshot or an explicit governance-change clause.
  • EIP-170 margin convention: the 24.175 KB artifact leaves ~401 bytes; the activation branch uses a 512-byte minimum production margin, so this should be an explicit exception or recover ~111 bytes, and the "one-line reversible" property only holds before reservations hold live state — after activation, changing the layout needs a state migration. Codex's strongest addition: sequence the merge so reservations are never activated on the temporary layout — deploy disabled (as the deploy script already does), rebase onto the router architecture, then enable the vault via governance. That removes the live-state-migration risk entirely.

Remaining follow-ups (all blocked on sequencing, not design)

  1. SDK (typescript/): typechain is generated from the published @keep-network/tbtc-v2 npm artifacts, so reservation methods cannot typecheck until this PR merges and publishes. The work itself is mechanical (vault routing in DepositsService + a thin reservations service).
  2. keep-core executor wiring + tbtcpg generation + Ethereum bindings: same artifact-publication dependency; the foundations (action types, proposals, chain interface, transaction assembly) are in draft: UTXO reservation wallet-side foundations keep-core#4238.
  3. Protobuf definitions for the coordination proposal types (TODO-marked in #4238).
  4. System-tests: regtest e2e with real wallet signing (contract-side validation is covered by the crafted SPV fixtures here).

Adds the Reservation library implementing segregated, in-kind-redeemable
custody of deposited UTXOs. A deposit revealed with the designated
reservation vault is anchored by the wallet -- a 1-input-1-output spend
into a fresh wallet-controlled output with no refund path -- instead of
being swept. Balance is credited gross only on the SPV proof of the
anchor, mirroring the sweep's refund-disabling role while dropping its
consolidating role. The registry tracks the anchor outpoint through
re-anchor hops (wallet migration) until an in-kind reserved redemption
burns the gross claim, or term+grace expiry lets the wallet dissolve
the anchor into its main UTXO.

All four lifecycle SPV proofs share one Bridge entry point
(submitReservationProof) to preserve the EIP-170 margin; Bridge compiles
to 23.9 KB with a runs=100 optimizer override (baseline 24.1 KB at
runs=1000 with only 0.5 KB of headroom).

Safety wiring: acceptance marks the deposit swept (blocking sweeps and
enabling fraud-challenge defeats); consumed anchors are recorded in
spentMainUTXOs so the existing defeat path recognizes them; sweeps
targeting the reservation vault revert; wallets cannot finalize closing
while custodying reservations; reserved redemptions pass the redemption
watchtower isSafeRedemption gate and reuse the redemption-timeout
slashing machinery.
Liability-side companion of the reservation core: receives the gross
acceptance credit, mints TBTC gross to the reservation owner, and
collects all protocol fees as explicit TBTC transfers (claims are never
netted, so the surrendered claim always equals the sats earmarked
on-chain). Draft fee schedule: 40 bps all-in at initiation (mint leg +
first custody term), 20 bps per extension, 20 bps at redemption --
strictly dominating the pooled path's 20+20 bps round trip at every
holding horizon.
Covers parameter governance, the reserved-deposit sweep guard (recorded
sweep proof against a reservation-routed deposit), term extension,
reserved redemption request/timeout bookkeeping with wallet-slashing
reuse, and the vault's gross-mint fee split and redemption surrender
flow. The SPV proof validators (acceptance, redemption, re-anchor,
dissolution) are exercised only up to proof validation and need
Bitcoin-fixture coverage as a follow-up.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d5c698d2-ee0e-4165-a2ac-e5909744d7ca

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The deploy-script-85 unit test mocks the deployments registry and the
rebate recovery test links BridgeStub libraries explicitly; both needed
to learn about the new Reservation library.
Hoists the TBTCVault mint out of the per-depositor loop (one aggregate
mint, per-depositor transfers), which also resolves the Slither
calls-inside-a-loop findings. Adds retryRedeemReservation so an owner
left holding Bank balance by a reserved-redemption timeout can re-request
without manually re-minting TBTC.
Mirrors Redemption.notifyRedemptionVeto for reserved redemptions: the
watchtower detains the surrendered gross balance, the pending request is
cleared, and the reservation returns to Active -- the anchor outpoint
was not spent, so the in-kind claim survives. The guardian-side flow in
the RedemptionWatchtower contract is a follow-up; this is the Bridge
hook it needs.
Adds a grouped begin/finalize pair to BridgeGovernance staging all
reservation parameters (including the reservation vault) behind the
standard governance delay, applied atomically via the Bridge's single
updateReservationParameters call.
Crafts structurally-valid Bitcoin transactions and regtest-difficulty
headers in pure TypeScript (SPV validation checks structure, merkle
inclusion, and header work -- not script signatures) and exercises all
four reservation proof validators end to end: anchor acceptance with
gross vault credit, in-kind redemption with gross burn, re-anchoring to
a second wallet, and post-grace dissolution into the main UTXO. Also
covers the watchtower veto, the vault retry path, and the
governance-delayed parameters flow.
Adds raiseReservedObjection mirroring raiseObjection for pending
reserved redemptions, reusing the existing VetoProposal/objections
storage keyed by reservation key -- no storage layout changes, so the
watchtower upgrade is layout-safe. Three objections finalize the veto:
the surrendered gross amount is detained via the Bridge's
notifyReservedRedemptionVeto hook, the penalty fee is burned, the
redeemer is banned, and the reservation returns to Active on the Bridge.
Also exposes getReservedRedemptionDelay for wallet-side coordination and
extends the IRedemptionWatchtower interface accordingly.
Teaches WalletProposalValidator the reservation lifecycle: rejects sweep
proposals containing reservation-vault deposits, and adds proposal
validation for anchors (deposit eligibility, fee bounds, minimum anchor
amount, refund safety margin), reserved redemptions (pending state,
watchtower delay, timeout safety margin, snapshotted fee bound),
re-anchors (Active state, Live target) and dissolutions (term + grace
elapsed). Restores the grouped Bridge.reservationParameters() getter the
validator consumes; Bridge stays under the EIP-170 limit (24.175 KB).
Covers the full guardian objection flow (three objections, veto
finalization through the Bridge hook, 100% penalty burn, redeemer ban
blocking re-requests via the isSafeRedemption gate) and the validator's
sweep exclusion, anchor, reserved redemption, re-anchor and dissolution
proposal rules.
Prettier reflowed the multi-line if condition, detaching the
no-await-in-loop disable comment from the awaited call.
Adopts the custody-style pay-to-hold schedule: 40 bps all-in at
initiation (mint leg + first-year custody), 20 bps per extension year,
free redemption. Never cheaper than the pooled 20+20 bps round trip
(parity at a one-year hold, premium beyond), exit-neutral where the
product's in-kind promise lives, and it removes the fee re-charge on
retries after wallet-fault timeouts. The redemptionFeeBps parameter is
retained for governance; the minimum reservation size -- not the fee
schedule -- is the dial that keeps carry covering per-position
lifecycle costs as FROST lowers ceremony economics.
The endpoints are priced at parity with the pooled path -- a 20 bps
mint leg inside the 40 bps initiation fee and a 20 bps redemption fee --
so the only premium being purchased is the 20 bps/yr custody fee (the
remainder of the initiation fee prepays the first year). An N-year
holding pays 40 + 20N bps against the pooled 40 bps round trip:
strictly premium at every horizon. The redemption fee is not re-charged
on retries after wallet-fault timeouts: it was collected by the
original request, and the retry only exists because the wallet failed.
Two correctness fixes surfaced by external review of the proof
validators:

1. Redemption liveness. The redemption range check subtracted
   redemptionTxMaxFee (a governable parameter, not a tx-derived value)
   from anchorAmount, which reverts on underflow in Solidity 0.8 when the
   fee bound exceeds the anchor -- reachable via a governance fee increase
   against an existing anchor. That would make every redemption proof for
   the affected reservation revert, stranding the coins until dissolution.
   Reformulated as the underflow-safe equivalent
   (outputValue <= anchorAmount && anchorAmount - outputValue <= maxFee).

2. Re-anchor floor. Re-anchors bounded only the per-hop miner fee, never
   the resulting anchor, so repeated network-scheduled hops could grind
   the anchor toward the fee bound. Now require the re-anchored amount to
   stay >= reservationMinAmount; since the minimum is required to exceed
   the tx max fee, this also keeps the anchor clear of the redemption fee
   bound, reinforcing fix (1) across migrations.

Adds regression tests for both.
…100 override

A runs=1000-vs-100 gas diff over the deposit/redemption/reservation hot
paths measures at 0-8 gas (noise): the Bridge is a dispatch shell over
linked libraries that stay at runs=1000, so the size-preserving override
carries no meaningful runtime cost.
External review found that the previous `>= reservationMinAmount`
re-anchor floor, combined with the proposal validator's positive-fee
requirement, left no compliant re-anchor for an exactly-minimum-sized
reservation — pinning a retiring wallet, which contradicts mandatory
migration. The floor's redemption-liveness purpose is already covered by
the underflow-safe redemption range check, so relax it to a dust floor
(`> reservationTxMaxFee`) that keeps anchors clear of dust while leaving
minimum-sized reservations migratable. Bounding cumulative Byzantine
re-anchor grinding is deferred to the authorized-action model (migration
request with nonce + owner/target authorization + cumulative fee
budget), tracked as a follow-up.

Regression test asserts a minimum-sized reservation migrates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant